"""Check a project and backend by attempting to build using PEP 517 hooks. """ import argparse import logging import os from os.path import isfile, join as pjoin from pip._vendor.toml import TomlDecodeError, load as toml_load import shutil from subprocess import CalledProcessError import sys import tarfile from tempfile import mkdtemp import zipfile from .colorlog import enable_colourful_output from .envbuild import BuildEnvironment from .wrappers import Pep517HookCaller log = logging.getLogger(__name__) def check_build_sdist(hooks, build_sys_requires): with BuildEnvironment() as env: try: env.pip_install(build_sys_requires) log.info('Installed static build dependencies') except CalledProcessError: log.error('Failed to install static build dependencies') return False try: reqs = hooks.get_requires_for_build_sdist({}) log.info('Got build requires: %s', reqs) except Exception: log.error('Failure in get_requires_for_build_sdist', exc_info=True) return False try: env.pip_install(reqs) log.info('Installed dynamic build dependencies') except CalledProcessError: log.error('Failed to install dynamic build dependencies') return False td = mkdtemp() log.info('Trying to build sdist in %s', td) try: try: filename = hooks.build_sdist(td, {}) log.info('build_sdist returned %r', filename) except Exception: log.info('Failure in build_sdist', exc_info=True) return False if not filename.endswith('.tar.gz'): log.error( "Filename %s doesn't have .tar.gz extension", filename) return False path = pjoin(td, filename) if isfile(path): log.info("Output file %s exists", path) else: log.error("Output file %s does not exist", path) return False if tarfile.is_tarfile(path): log.info("Output file is a tar file") else: log.error("Output file is not a tar file") return False finally: shutil.rmtree(td) return True def check_build_wheel(hooks, build_sys_requires): with BuildEnvironment() as env: try: env.pip_install(build_sys_requires) log.info('Installed static build dependencies') except CalledProcessError: log.error('Failed to install static build dependencies') return False try: reqs = hooks.get_requires_for_build_wheel({}) log.info('Got build requires: %s', reqs) except Exception: log.error('Failure in get_requires_for_build_sdist', exc_info=True) return False try: env.pip_install(reqs) log.info('Installed dynamic build dependencies') except CalledProcessError: log.error('Failed to install dynamic build dependencies') return False td = mkdtemp() log.info('Trying to build wheel in %s', td) try: try: filename = hooks.build_wheel(td, {}) log.info('build_wheel returned %r', filename) except Exception: log.info('Failure in build_wheel', exc_info=True) return False if not filename.endswith('.whl'): log.error("Filename %s doesn't have .whl extension", filename) return False path = pjoin(td, filename) if isfile(path): log.info("Output file %s exists", path) else: log.error("Output file %s does not exist", path) return False if zipfile.is_zipfile(path): log.info("Output file is a zip file") else: log.error("Output file is not a zip file") return False finally: shutil.rmtree(td) return True def check(source_dir): pyproject = pjoin(source_dir, 'pyproject.toml') if isfile(pyproject): log.info('Found pyproject.toml') else: log.error('Missing pyproject.toml') return False try: with open(pyproject) as f: pyproject_data = toml_load(f) # Ensure the mandatory data can be loaded buildsys = pyproject_data['build-system'] requires = buildsys['requires'] backend = buildsys['build-backend'] backend_path = buildsys.get('backend-path') log.info('Loaded pyproject.toml') except (TomlDecodeError, KeyError): log.error("Invalid pyproject.toml", exc_info=True) return False hooks = Pep517HookCaller(source_dir, backend, backend_path) sdist_ok = check_build_sdist(hooks, requires) wheel_ok = check_build_wheel(hooks, requires) if not sdist_ok: log.warning('Sdist checks failed; scroll up to see') if not wheel_ok: log.warning('Wheel checks failed') return sdist_ok def main(argv=None): ap = argparse.ArgumentParser() ap.add_argument( 'source_dir', help="A directory containing pyproject.toml") args = ap.parse_args(argv) enable_colourful_output() ok = check(args.source_dir) if ok: print(ansi('Checks passed', 'green')) else: print(ansi('Checks failed', 'red')) sys.exit(1) ansi_codes = { 'reset': '\x1b[0m', 'bold': '\x1b[1m', 'red': '\x1b[31m', 'green': '\x1b[32m', } def ansi(s, attr): if os.name != 'nt' and sys.stdout.isatty(): return ansi_codes[attr] + str(s) + ansi_codes['reset'] else: return str(s) if __name__ == '__main__': main()